package org.fastiot.protocol.group

import grails.databinding.BindUsing
import org.fastiot.protocol.codec.BitOutputStream
import org.fastiot.protocol.codec.BitSource
import org.fastiot.protocol.Metadata

/**
 * （公式）计算编组
 * 根据预设公式，在编解码上下文环境中，通过计算获取实际长度
 */
class Calculation implements Groupable {

    static enum Scene {
        Encode(0), Decode(1), Both(9)
        final int value

        private Scene(int value) { this.value = value }

        static Scene byValue(int val) {
            values().find { it.value == val } ?:
                    { throw new IllegalArgumentException("Calculation\$Scene ${val} not found") }()
        }
    }

    /**
     * 隶属（结构/单元）/父类型
     */
    @BindUsing({ obj, source ->
        Affiliation.byValue(Integer.parseInt(source["parentType"] as String))
    })
    Affiliation parentType

    /**
     * 隶属（结构/单元）代码
     */
    Integer parentCode
    /**
     * 隶属（结构/单元）代码（Hex）
     */
    String hexParentCode

    /**
     * （编组）名称
     */
    String name
    /**
     * 排列序号
     */
    Integer serialNo
    /**
     * 数据/编组标识符
     */
    String dataIdentifier
    /**
     * （计算）场景
     */
    Scene scene
    /**
     * （计算）公式，groovy script text
     */
    String formula

    /**
     * （基础）规则代码
     */
    String ruleCode

    /**
     * （编解码）规则模式
     */
    @BindUsing({ obj, source ->
        RuleMode.byValue(Integer.parseInt(source["ruleMode"] as String))
    })
    RuleMode ruleMode

    /**
     * 内置单元类型
     */
    @BindUsing({ obj, source ->
        Groupable.Type.byValue(Integer.parseInt(source["embeddedGroupType"] as String))
    })
    Groupable.Type embeddedGroupType

    /**
     * 内置单元隶属代码
     */
    Integer embeddedGroupAffiliatedCode

    boolean enabled = false

    String remark
    Date dateCreated
    Date lastUpdated

    private static final groovyLoader = new GroovyClassLoader()
    private static final CachedFormulaScripts = [:]

    static transients = ['hexParentCode']

    static constraints = {
        parentType(nullable: false, inList: Affiliation.values().toList())
        parentCode(nullable: false)
        hexParentCode(nullable: false, matches: /[a-fA-F0-9]+/)
        serialNo(nullable: false, min: 1)
        dataIdentifier(blank: false)
        scene(nullable: false)
        formula(blank: false)
        name(blank: false, unique: true)
        ruleCode(nullable: false, unique: ["parentType", "parentCode"], matches: /[a-zA-Z0-9]+/)
        ruleMode(nullable: false)
        embeddedGroupType(nullable: true, inList: Groupable.Type.values().toList(), validator: Calculation.ValidateEmbeddedRules)
        embeddedGroupAffiliatedCode(nullable: true, validator: Calculation.ValidateEmbeddedRules, min: 0)
        enabled(blank: false)
        remark(nullable: true, widget: 'textarea')
    }

    static ValidateEmbeddedRules = { val, obj, errors ->
        if (obj.ruleMode == RuleMode.BasicRule && val != null) {
            errors.rejectValue("ruleMode", "Properties of nested engine must be NULL when 'BasicRule' is selected")
        } else if (obj.ruleMode == RuleMode.EmbeddedGroup && val == null) {
            errors.rejectValue("ruleMode", "Properties of nested engine must be set when 'EmbeddedGroup' is selected")
        }
    }

    static mapping = {
        remark type: 'text'
        sort serialNo: "asc"
    }

    def beforeValidate() {
        if (hexParentCode && !parentCode)
            parentCode = Integer.parseInt(hexParentCode, 16)

        if(ruleMode == RuleMode.BasicRule && ruleCode && !dataIdentifier) {
            dataIdentifier = ruleCode
        }
    }

    def afterLoad() {
        if (parentCode)
            hexParentCode = Integer.toHexString(parentCode)

        getLengthByFormula(null, true)
    }

    private def getLengthByFormula(final Object sources, boolean forcedInit = false) {
        if (formula || forcedInit) {
            def key = "$parentType[$parentCode]-$ruleCode" as String
            def scriptClass = CachedFormulaScripts[key]

            if (!scriptClass) {
                scriptClass = CachedFormulaScripts[key] = groovyLoader.parseClass(formula)
            }

            if (scriptClass && sources) {
                def script = scriptClass.newInstance()
                script.binding.setVariable("x", sources)
                script.run()
            } else {
                sources
            }
        } else {
            sources
        }
    }

    @Override
    void decode(final BitSource bitSource, final Map contents) {
        if (formula && (scene == Scene.Decode || scene == Scene.Both)) {
            def length = getLengthByFormula(contents)

            if (length > 0) {
                if (ruleMode == RuleMode.BasicRule) {
                    1.upto(length) {
                        def basicRule = Metadata.CachedBasicRules[ruleCode]
                        contents[dataIdentifier] = basicRule.decode(bitSource)
                    }
                } else if (ruleMode == RuleMode.EmbeddedGroup) {
                    def embeddedContents = contents.get(dataIdentifier, [:])

                    1.upto(length) {
                        for (def unit : Metadata.CachedGroups[embeddedGroupType][Groupable.Type.Calculation][embeddedGroupAffiliatedCode]) {
                            unit.decode(bitSource, embeddedContents)
                        }
                    }

                    if (log.isDebugEnabled()) {
                        log.debug "Decoding Group[Calculation] EmbeddedGroup $formula>>$length>>$contents"
                    }
                }
            }

        } else {//BasicRule & non-formula
            def basicRule = Metadata.CachedBasicRules[ruleCode]
            contents[dataIdentifier] = basicRule.decode(bitSource)
        }
    }

    @Override
    void encode(final BitOutputStream bitOutput, final Map contents) {
        if (formula && (scene == Scene.Encode || scene == Scene.Both)) {
            def length = getLengthByFormula(contents)

            if (length > 0) {
                if (ruleMode == RuleMode.BasicRule) {
                    1.upto(length) {
                        def basicRule = Metadata.CachedBasicRules[ruleCode]
                        basicRule.encode(bitOutput, contents[dataIdentifier])
                    }
                } else if (ruleMode == RuleMode.EmbeddedGroup) {
                    def embeddedContents = contents.get(dataIdentifier, [:])

                    1.upto(length) {
                        for (def unit : Metadata.CachedGroups[embeddedGroupType][Groupable.Type.Calculation][embeddedGroupAffiliatedCode]) {
                            unit.encode(bitOutput, embeddedContents)
                        }
                    }

                    if (log.isDebugEnabled()) {
                        log.debug "Encoding Group[Calculation] EmbeddedGroup $formula>>$length>>$embeddedContents"
                    }
                }
            }
        } else {
            def basicRule = Metadata.CachedBasicRules[ruleCode]
            basicRule.encode(bitOutput, contents)
        }
    }
}
